/*
Copyright (c) 2007,2007 Elizeu Nogueira da R. Jr.

All rights reserved.

Redistribution and use in source and binary forms, with or without modification, 
are permitted provided that the following conditions are met:

    * Redistributions of source code must retain the above copyright notice, 
      this list of conditions and the following disclaimer.
    * Redistributions in binary form must reproduce the above copyright notice, 
      this list of conditions and the following disclaimer in the documentation 
      and/or other materials provided with the distribution.
    * Neither the name of the Elizeu Nogueira da R. Jr. nor the names of its 
      contributors may be used to endorse or promote products derived from this 
      software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.


This package is based on HypersonicSQL, originally developed by Thomas Mueller.
and HSQLDB original developed by the HSQL Development Group
*/

package br.com.dreamsource.mobile.jmesql;

import java.io.*;
import java.util.*;
import br.com.dreamsource.mobile.jmesql.io.DBFileReaderWriter;
import br.com.dreamsource.mobile.jmesql.io.File;
import br.com.dreamsource.mobile.jmesql.io.Properties;
import br.com.dreamsource.mobile.jmesql.io.LineNumberReader;
import br.com.dreamsource.mobile.jmesql.exceptions.SQLException;
import br.com.dreamsource.mobile.jmesql.io.RandomAccessFile;

class Log implements Runnable {
    private final static int COPY_BLOCK_SIZE =
	1 << 16;			     // block size for copying data
    private Properties       pProperties;
    private String	     sName;
    private Databases	     dDatabase;
    private Channel	     cSystem;
//    private Writer	     wScript;
    private LineNumberReader wScript;    
    private String	     sFileProperties;
    private String	     sFileScript;
    private String	     sFileCache;
    private String	     sFileBackup;
    private boolean	     bRestoring;
    private boolean	     bReadOnly;
    private int		     iLogSize =
	200;				     // default: .script file is max 200 MB big
    private int		     iLogCount;
    private Thread	     tRunner;
    private volatile boolean bNeedFlush;
    private volatile boolean bWriteDelay;
    private int		     mLastId;
    Cache		     cCache;

    Log(Databases db, Channel system, String name) throws SQLException {
	dDatabase = db;
	cSystem = system;
	sName = name;
	sFileProperties = sName + ".properties";
	pProperties = new Properties();
        pProperties.setFileName(sName + ".properties");
        
	tRunner = new Thread(this);
	tRunner.start();
    }

    public void run() {
	while (tRunner != null) {
	    try {
		tRunner.sleep(1000);

		if (bNeedFlush) {
		    wScript.flush();

		    bNeedFlush = false;
		}

		// todo: try to do Cache.cleanUp() here, too
	    } catch (Exception e) {

		// ignore exceptions; may be InterruptedException or IOException
	    }
	}
    }

    void setWriteDelay(boolean delay) {
	bWriteDelay = delay;
    }

    boolean open() throws SQLException {
	if (Trace.TRACE) {
	    Trace.trace();
	}

        if (! pProperties.checkFileExists()) {
	    create();
	    open();
	    return true;
        }
        
        pProperties.open();

	// todo: some parts are not necessary for ready-only access

	sFileScript = pProperties.getProperty("script", sName + ".script");
	sFileCache = pProperties.getProperty("data", sName + ".data");
	sFileBackup = pProperties.getProperty("backup", sName + ".backup");

	String  version = pProperties.getProperty("version", "1.0");
	boolean check = version.substring(0,3).compareTo(jdbcDriver.VERSION) <= 0;
        
       pProperties.setProperty("version", jdbcDriver.VERSION);


	Trace.check(check, Trace.WRONG_DATABASE_FILE_VERSION);

	if (pProperties.getProperty("readonly", "false").equals("true")) {
	    bReadOnly = true;

	    dDatabase.setReadOnly();

	    cCache = new Cache(sFileCache);

	    cCache.open(true);
	    runScript();

	    return false;
	}

	boolean needbackup = false;
	String  state = pProperties.getProperty("modified", "no");

	if (state.equals("yes-new-files")) {
	    renameNewToCurrent(sFileScript);
	    renameNewToCurrent(sFileBackup);
	} else if (state.equals("yes")) {
	    needbackup = true;
	}

	pProperties.put("modified", "yes");
        pProperties.save();
	cCache = new Cache(sFileCache);
	cCache.open(false);
	runScript();
	openScript();
	return false;
    }

    void stop() {
	tRunner = null;
    }

    void close(boolean compact) throws SQLException {
	if (Trace.TRACE) {
	    Trace.trace();
	}

	if (bReadOnly) {
	    return;
	}

	// no more scripting
	closeScript();

	// create '.script.new' (for this the cache may be still required)
	writeScript(compact);

	// flush the cache (important: after writing the script)
	cCache.flush();

	// create '.backup.new' using the '.data'

	// we have the new files
	pProperties.put("modified", "yes-new-files");
	pProperties.save();

	// old files can be removed and new files renamed
	renameNewToCurrent(sFileScript);
	renameNewToCurrent(sFileBackup);

	// now its done completely
	pProperties.put("modified", "no");
	pProperties.save();

    }

    void checkpoint() throws SQLException {
	close(false);
	pProperties.put("modified", "yes");
	pProperties.save();
	cCache.open(false);
	openScript();
    }

    void setLogSize(int mb) {
	iLogSize = mb;
    }
    
    void updateLine(String s, String newS) throws SQLException {
        wScript.updateLine(s, newS);
    }

/* TODO Elizeu    
    void deleteLine(String s) throws SQLException {
        wScript.deleteLine(s);
    }
*/
    void write(Channel c, String s) throws SQLException {
	if (bRestoring || s == null || s.equals("")) {
	    return;
	}

	if (!bReadOnly) {
		int id = 0;

		if (c != null) {
		    id = c.getId();
		}

		if (id != mLastId) {
		    s = "/*C" + id + "*/" + s;
		    mLastId = id;
		}

		try {
		    //writeLine(wScript, s);
                    wScript.writeLine(s);
		    if (bWriteDelay) {
			bNeedFlush = true;
		    } else {
			wScript.flush();
		    }
		} catch (Exception e) {
		    Trace.error(Trace.FILE_IO_ERROR, sFileScript);
		}

		if (iLogSize > 0 && iLogCount++ > 100) {
		    iLogCount = 0;
                    
		    //if ((new File(sFileScript)).length() > iLogSize * 1024 * 1024) {
		    //		checkpoint();
		    //}
		}
	}

    }

    void shutdown() throws SQLException {
	tRunner = null;

	cCache.shutdown();
	closeScript();
	pProperties.close();
    }

    static void scriptToFile(Databases db, String file, boolean full,
			     Channel channel) throws SQLException {
        
	if (new File(file).exists()) {

	    // there must be no such file; overwriting not allowed for security
	    throw Trace.error(Trace.FILE_IO_ERROR, file);
	}

	try {
	    long   time = System.currentTimeMillis();

	    // only ddl commands; needs not so much memory
	    Result r;

	    if (full) {

		// no drop, no insert, and no positions for cached tables
		r = db.getScript(false, false, false, channel);
	    } else {

		// no drop, no insert, but positions for cached tables
		r = db.getScript(false, false, true, channel);
	    }

	    Record     n = r.rRoot;
            DBFileReaderWriter DBRW = new DBFileReaderWriter(file);
//	    FileWriter w = new FileWriter(file);

	    while (n != null) {
		//writeLine(w, (String) n.data[0]);
                DBRW.writeLine(StringConverter.unicodeToAscii((String) n.data[0]));
		n = n.next;
	    }

	    // inserts are done separetely to save memory
	    Vector tables = db.getTables();

	    for (int i = 0; i < tables.size(); i++) {
		Table t = (Table) tables.elementAt(i);

		// cached tables have the index roots set in the ddl script
		if (full ||!t.isCached()) {
		    Index primary = t.getPrimaryIndex();
		    Node  x = primary.first();

		    while (x != null) {
//			writeLine(w, t.getInsertStatement(x.getData()));
                        DBRW.writeLine(t.getInsertStatement(x.getData()));

			x = primary.next(x);
		    }
		}
	    }

	    //w.close();
            DBRW.close();

	    time = System.currentTimeMillis() - time;

	    if (Trace.TRACE) {
		Trace.trace(time);
	    }
	} catch (Exception e) {
	    Trace.error(Trace.FILE_IO_ERROR, file + " " + e);
	}
    }

    private void renameNewToCurrent(String file) {

	// even if it crashes here, recovering is no problem
        try {
            if ((new File(file + ".new")).exists()) {

                // if we have a new file
                // delete the old (maybe already deleted)
                (new File(file)).delete();

                // rename the new to the current
                new File(file + ".new").renameTo(file);
            }
        }
        catch (Exception e) {
            System.out.println(e.getMessage());
        }
    }
    
    private void create() throws SQLException {
	if (Trace.TRACE) {
	    Trace.trace(sName);
	}
	pProperties.put("modified", "no");
	pProperties.put("version", jdbcDriver.VERSION);
	pProperties.save();
        pProperties.close();
    }

    private void openScript() throws SQLException {
	if (Trace.TRACE) {
	    Trace.trace();
	}
	try {
	    // todo: use a compressed stream
	    //wScript = new BufferedWriter(new FileWriter(sFileScript, true), 4096);
            wScript = new LineNumberReader(sFileScript);
	} catch (Exception e) {
	    Trace.error(Trace.FILE_IO_ERROR, sFileScript);
	}
    }

    private void closeScript() throws SQLException {
	if (Trace.TRACE) {
	    Trace.trace();
	}

	try {
	    if (wScript != null) {
		//wScript.close();

		wScript = null;
	    }
	} catch (Exception e) {
	    Trace.error(Trace.FILE_IO_ERROR, sFileScript);
	}
    }

    private void runScript() throws SQLException {
	if (Trace.TRACE) {
	    Trace.trace();

	}

	if (!(new File(sFileScript)).exists()) {
	    return;
	}

	bRestoring = true;

	dDatabase.setReferentialIntegrity(false);

	Vector channel = new Vector();

	channel.addElement(cSystem);

	Channel current = cSystem;
	int     size = 1;

	try {
	    long	     time = System.currentTimeMillis();
	    //LineNumberReader r =
		//new LineNumberReader(new FileReader(sFileScript));
            LineNumberReader R = new LineNumberReader(sFileScript);

	    while (true) {

		String s = readLine(R);
                //System.out.println(s);
		if (s == null) {
		    break;
		}
                
		if (s.startsWith("/*C")) {
		    int id = Integer.parseInt(s.substring(3,
							  s.indexOf('*', 4)));

		    if (id >= size) {
			channel.setSize(id + 1);
		    }

		    current = (Channel) channel.elementAt(id);

		    if (current == null) {
			current = new Channel(cSystem, id);

			channel.setElementAt(current, id);
			dDatabase.registerChannel(current);
		    }

		    s = s.substring(s.indexOf('/', 1) + 1);
		}

		if (!s.equals("")) {
		    dDatabase.execute(s, current);
		}

		if (s.equals("DISCONNECT")) {
		    int id = current.getId();

		    current = new Channel(cSystem, id);

		    channel.setElementAt(current, id);
		}
	    }
	    //R.close();

	    for (int i = 0; i < size; i++) {
		current = (Channel) channel.elementAt(i);

		if (current != null) {
		    current.rollback();
		}
	    }

	    time = System.currentTimeMillis() - time;

	    if (Trace.TRACE) {
		Trace.trace(time);
	    }
	} catch (IOException e) {
	    throw Trace.error(Trace.FILE_IO_ERROR, sFileScript + " " + e);
	}

	dDatabase.setReferentialIntegrity(true);

	bRestoring = false;
    }

    private void writeScript(boolean full) throws SQLException {
	if (Trace.TRACE) {
	    Trace.trace();

	    // create script in '.new' file
	}

	(new File(sFileScript + ".new")).delete();

	// script; but only positions of cached tables, not full
	scriptToFile(dDatabase, sFileScript + ".new", full, cSystem);
    }

    private static void writeLine(Writer w, String s) throws IOException {
	w.write(StringConverter.unicodeToAscii(s) + "\r\n");
    }

    private static String readLine(LineNumberReader r) throws IOException {
        try {
            String s = r.readLine();
            return StringConverter.asciiToUnicode(s);
        }
        catch (Exception e) {
            throw new IOException(e.getMessage());
        }
    }

}
